package model.management;

import gui.item.ItemData;

import java.util.*;

import model.entity.INode;
import model.entity.Item;
import model.entity.Product;
import model.entity.container.ProductContainer;
import model.entity.container.StorageUnit;
import model.events.EntityEvent;
import model.events.EntityEventEmitter;
import model.events.EntityModifiedEvent;
import persistence.ItemsDAO;
import persistence.PersistenceMode;
import reports.visitor.IVisitor;

import common.Barcode;
import common.Result;
import common.ValueResult;
import common.util.DateUtils;

/**
 * Tracks the instance of the items in the database. Also provides functionality for running
 * reports on items.
 * 
 * @author Ian Ryu, Ryan Cheatham
 * 
 *         {@invariant items.size >= 0}
 */
public class ItemManager extends EntityEventEmitter implements INode
{
  private static ItemManager INSTANCE = new ItemManager();

  public static ItemManager getInstance()
  {
    return INSTANCE;
  }

  private Set<Item>               items          = new HashSet<Item>();
  private Set<Item>               removedItems   = new HashSet<Item>();

  private Map<String, Item>       itemsByBarcode = new HashMap<String, Item>();
  private Map<Product, Set<Item>> itemsByProduct = new HashMap<Product, Set<Item>>();

  private ItemManager()
  {
    load();
  }

  private int currentItemBarcodeID = 1;

  /**
   * Validates whether the given item can be added as described.
   * 
   * @param item
   *          The item to validate.
   * @return a result, whose status indicates whether the given item can be added to this
   *         container.
   * 
   *         {@pre item != null}
   */
  public static Result validateAddItem(Item item)
  {
    return INSTANCE._validateAddItem(item);
  }

  /**
   * Validates whether the given item can be added to this item container. If so, the item is added
   * to this container.
   * 
   * @param item
   *          The item to be added to this container.
   * 
   * @return a result, indicating whether this operation was successful or not. {@pre item != null}
   *         {@post items.size == (old)items.size() + 1}
   * 
   */
  public static ValueResult<EntityEvent> addItem(Item item)
  {
    return INSTANCE._addItem(item);
  }

  /**
   * Validates whether the given item can be moved to the given storage unit.
   * 
   * @param item
   *          The item to move
   * @param newUnit
   *          The new storage unit to move the item to
   * @return a result, indicating whether the move is acceptable.
   * 
   *         {@pre item != null} {@pre newUnit != null} {@post retval.getStatus() == true iff the
   *         move will be successful if attempted}
   */
  public static Result validateMoveItemToStorageUnit(Item item, ProductContainer newUnit)
  {
    return INSTANCE._validateMoveItemToStorageUnit(item, newUnit);
  }

  /**
   * Attempts to move the given item to the given storage unit.
   * 
   * @param item
   *          The item to move
   * @param newUnit
   *          The new storage unit to move the item to
   * @return a result, indicating whether the move was successful.
   * 
   *         {@pre item != null}{@pre newUnit != null}
   * 
   *         {@post item top level unit == newUnit}
   * 
   * @see #validateMoveItemToStorageUnit(Item, StorageUnit)
   */
  public static ValueResult<EntityEvent> moveItemToStorageUnit(Item item, ProductContainer newUnit)
  {
    return INSTANCE._moveItemToStorageUnit(item, newUnit);
  }

  /**
   * Validates whether the given item can be moved to the given product container.
   * 
   * @param items
   *          The item to move
   * @param newUnit
   *          The new container to move the item to
   * @return a result, indicating whether the move is acceptable.
   * 
   *         {@pre item != null} {@pre newContainer != null} {@post retval.getStatus() == true iff
   *         the move will be successful if attempted}
   */
  public static Result validateMoveItemsToProductContainer(Iterator<Item> items,
      ProductContainer newContainer)
  {
    return INSTANCE._validateMoveItemsToProductContainer(items, newContainer);
  }

  /**
   * Attempts to move the given item to the given product container.
   * 
   * @param items
   *          The item to move
   * @param newUnit
   *          The new container to move the item to
   * @return a result, indicating whether the move was successful.
   * 
   *         {@pre item != null}{@pre newContainer != null}
   * 
   *         {@post item top level unit == newUnit}
   * 
   * @see #validateMoveItemToStorageUnit(Item, StorageUnit)
   */
  public static Result moveItemsToProductContainer(Iterator<Item> items,
      ProductContainer newContainer)
  {
    return INSTANCE._moveItemsToProductContainer(items, newContainer);
  }

  public static Result moveItemToProductContainer(Item item, ProductContainer container)
  {
    return INSTANCE._moveItemToProductContainer(item, container);
  }

  /**
   * Validates whether the given item can be removed from items.
   * 
   * @param item
   *          The item to validate.
   * @return a result, whose status indicates whether the given item can be removed from this
   *         container.
   * 
   *         {@pre item != null}
   */
  public static Result validateRemoveItem(Item item)
  {
    return INSTANCE._validateRemoveItem(item);
  }

  /**
   * Validates whether the given item can be removed from this item container. If so, the item is
   * removed from this container.
   * 
   * @param item
   *          The item to be removed from this container.
   * 
   * @return a result, indicating whether this operation was successful or not.
   * 
   *         {@pre item != null} {@post items.size == (old)items.size() - 1}
   */
  public static ValueResult<EntityEvent> removeItem(Item item)
  {
    return INSTANCE._removeItem(item);
  }

  /**
   * @param before
   *          The <code>Item</code> in its initial state.
   * @param after
   *          The <code>Item</code> in its final state.
   * @return A <code>Result</code> indicating whether the proposed modification is valid.
   */
  public static Result validateUpdateItem(Item before, Item after)
  {
    return INSTANCE._validateUpdateItem(before, after);
  }

  /**
   * @param before
   *          The <code>Item</code> in its initial state.
   * @param after
   *          The <code>Item</code> in its final state.
   * @return A <code>Result</code> inidicating whether the operation was successful.
   */
  public static Result updateItem(Item before, Item after)
  {
    return INSTANCE._updateItem(before, after);
  }

  /**
   * @return An iterator of all <code>Product</code>s in the system.
   */
  public static Iterator<Item> getItems()
  {
    return INSTANCE._getItems();
  }

  /**
   * Gets the items in the given product container of the given product. This method searches
   * recursively.
   * 
   * @param container
   *          The container to search in
   * @param product
   *          The product to search for
   * @return An iterator through the list of items found
   * 
   * @see ProductContainer#getItems()
   */
  public static Iterator<Item> getItems(ProductContainer container, Product product)
  {
    return container.getItems(product);
  }

  /**
   * Generates and returns a new {@link Barcode} to be used to identify a unique item in the
   * database.
   * 
   * {@post retval is a valid, unique internal barcode}
   */
  public static String generateNewItemBarcode()
  {
    return INSTANCE._generateNewItemBarcode();
  }

  public static void clear()
  {
    INSTANCE.items.clear();
  }

  /**
   * Gets the items in the given container of the given product, and returns them as an array of
   * ItemData objects.
   * 
   * @param container
   *          the container to search in.
   * @param product
   *          the product that describes the items to search for
   * @return a collection of ItemData objects
   */
  public static Collection<ItemData> getItemData(ProductContainer container, Product product)
  {
    return INSTANCE._getItems(container, product);
  }

  /**
   * Validates whether the item can be modified to reflect the given item data.
   * 
   * @param before
   *          The item to possibly edit.
   * @param after
   *          The item data that describes the new state of the item.
   */
  public static Result validateEditItem(Item before, ItemData after)
  {
    return INSTANCE._validateEditItem(before, after);
  }

  /**
   * Validates and edits the item data
   * 
   * @param target
   *          The item to edit.
   * @param data
   *          The item data that describes the new state of the item.
   */
  public static Result editItem(Item target, ItemData data)
  {
    return INSTANCE._editItem(target, data);
  }

  public static Item getItem(String barcode)
  {
    return INSTANCE._getItem(barcode);
  }

  public static Comparator<ItemData> getItemSorterByDate()
  {
    return new Comparator<ItemData>()
    {
      @Override
      public int compare(ItemData o1, ItemData o2)
      {
        int value = o1.getEntryDate().compareTo(o2.getEntryDate());
        if (value == 0)
          value = o1.getBarcode().compareTo(o2.getBarcode());
        return value;
      }
    };
  }

  public static EntityEvent generateItemAddedEvent(Item item)
  {
    return new EntityEvent(item, EntityEvent.ENTITY_ADDED, null, item.getContainer());
  }

  private EntityEvent generateItemRemovedEvent(Item item)
  {
    return new EntityEvent(item, EntityEvent.ENTITY_REMOVED, item.getContainer(), null);
  }

  private EntityEvent generateItemMovedEvent(Item item, ProductContainer oldContainer)
  {
    return new EntityEvent(item, EntityEvent.ENTITY_ADDED, oldContainer, item.getContainer());
  }

  private EntityModifiedEvent generateItemModifiedEvent(Item before, Item after)
  {
    return new EntityModifiedEvent(before, after);
  }

  /**
   * Gets a list of all items that are expired.
   * 
   * @return a List of expired items.
   * 
   *         {@pre items != null}
   */
  public List<Item> getExpiredItems()
  {
    List<Item> expiredItems = new ArrayList<Item>();

    for (Item item : items)
    {
      if (item.getExpirationDate().compareTo(new Date()) < 0)
      {
        expiredItems.add(item);
      }
    }
    return expiredItems;
  }

  /**
   * Validates whether items are removed or not.
   * 
   * @param since
   *          The date user want to report from. (type Date).
   * 
   * @return a List of expired items.
   * 
   *         {@pre since != null}
   */
  public HashMap<Product, Integer> getRemovedItems(Date since)
  {
    HashMap<Product, Integer> removed = new HashMap<Product, Integer>();

    for (Item item : removedItems)
    {
      if (item.getExitTime() != null && item.getExitTime().compareTo(since) <= 0)
      {
        if (removed.get(item.getProduct()) != null)
        {
          removed.put(item.getProduct(), removed.get(item.getProduct()) + 1);
        }
        else
        {
          removed.put(item.getProduct(), 1);
        }

      }
    }
    return removed;
  }

  private Result _validateAddItem(Item item)
  {
    if (item == null)
    {
      return new Result(false, "Error: Null Item");
    }

    if (INSTANCE.items.contains(item))
    {
      return new Result(false, "Error: item is already in container.");
    }
    return new Result(true);
  }

  private ValueResult<EntityEvent> _addItem(Item item)
  {
    Result result = validateAddItem(item);
    if (!result.getStatus())
    {
      ValueResult<EntityEvent> r = new ValueResult<EntityEvent>();
      r.setStatus(false);
      r.setMessage(result.getMessage());
    }

    index(item);

    ProductManager.itemAdded(item);
    item.getContainer().addItem(item);

    EntityEvent event = generateItemAddedEvent(item);
    fireAddedEvent(event);

    save(); // For serialize mode; does nothing in sql mode
    PersistenceMode.getSystemMode().getItemsDAO().add(item);

    return new ValueResult<EntityEvent>(true, "", event);
  }

  private void index(Item item)
  {
    items.add(item);
    itemsByBarcode.put(item.getBarcode().getCode(), item);

    Set<Item> productList = itemsByProduct.get(item.getProduct());
    if (productList == null)
    {
      productList = new HashSet<Item>();
      itemsByProduct.put(item.getProduct(), productList);
    }
    productList.add(item);
  }

  private void unIndex(Item item)
  {
    items.remove(item);
    itemsByBarcode.remove(item.getBarcode().getCode());
    itemsByProduct.get(item.getProduct()).remove(item);
    item.setExitTime(DateUtils.currentDate());
    removedItems.add(item);

    StorageUnit unit = item.getContainer().getStorageUnit();
    unit.removeItem(item);
  }

  private Result _validateMoveItemsToProductContainer(Iterator<Item> items,
      ProductContainer newContainer)
  {
    return new Result(true);
  }

  private Result _moveItemsToProductContainer(Iterator<Item> items, ProductContainer newContainer)
  {
    Result result = _validateMoveItemsToProductContainer(items, newContainer);
    if (result.getStatus() == false)
      return result;

    Item item = null;
    ProductContainer oldContainer = null;
    while (items.hasNext())
    {
      Item i = items.next();
      if (item == null)
      {
        item = i;
        oldContainer = i.getContainer();
      }
      i.setContainer(newContainer);
    }

    if (item != null)
    {
      EntityEvent event = generateItemMovedEvent(item, oldContainer);
      fireMovedEvent(event);
    }

    return result;
  }

  private Result _moveItemToProductContainer(Item item, ProductContainer container)
  {
    Result result;
    Product product = item.getProduct();
    StorageUnit unit = container.getStorageUnit();
    ProductContainer oldContainer = item.getContainer();
    ProductContainer newContainer = unit.getProductContainer(product);

    if (newContainer == null)
    {
      result = ProductManager.addProductToContainer(container, product);
      if (result.getStatus() == false)
        return result;

      item.setContainer(container);
    }
    else
    {
      Iterator<Item> itr = oldContainer.getItems(product);
      Collection<Item> items = new ArrayList<Item>();
      while (itr.hasNext())
        items.add(itr.next());

      for (Item i : items)
        i.setContainer(container);

      result = ProductManager.removeProductFromContainer(product, oldContainer);
      if (result.getStatus() == false)
        return result;
    }

    EntityEvent event = generateItemMovedEvent(item, oldContainer);
    fireMovedEvent(event);

    return result;
  }

  private Result _validateMoveItemToStorageUnit(Item item, ProductContainer newUnit)
  {
    if (item.getExitTime() != null)
      return new Result(false, "Cannot move item - it has already been removed from storage!");
    else
      return new Result(true);
  }

  private ValueResult<EntityEvent> _moveItemToStorageUnit(Item item, ProductContainer newUnit)
  {
    Result result = _validateMoveItemToStorageUnit(item, newUnit);
    if (result.getStatus() == false)
      return new ValueResult<EntityEvent>(false, result.getMessage());

    ProductContainer oldContainer = item.getContainer();
    ProductContainer newContainer = newUnit.getProductContainer(item.getProduct());
    if (newContainer == null)
      newContainer = newUnit;

    item.setContainer(newContainer);

    EntityEvent event = generateItemMovedEvent(item, oldContainer);
    fireMovedEvent(event);

    return new ValueResult<EntityEvent>(true, "", event);
  }

  private Result _validateRemoveItem(Item item)
  {
    if (item == null)
    {
      return new Result(false, "Error: item is Null");
    }
    if (!items.contains(item))
    {
      return new Result(false, "Error: item is not in system.");
    }
    return new Result(true);
  }

  private ValueResult<EntityEvent> _removeItem(Item item)
  {
    Result result = _validateRemoveItem(item);
    if (!result.getStatus())
    {
      return new ValueResult<EntityEvent>(result.getStatus(), result.getMessage());
    }

    item.setExitTime(new Date());
    ItemsDAO dao = PersistenceMode.getSystemMode().getItemsDAO();
    dao.update(item);
    unIndex(item);

    EntityEvent event = generateItemRemovedEvent(item);
    fireRemovedEvent(event);

    return new ValueResult<EntityEvent>(true, "", event);
  }

  private Result _validateUpdateItem(Item oldItem, Item newItem)
  {
    if (oldItem == null || newItem == null)
    {
      return new Result(false, "Error: Null item");
    }

    if (!INSTANCE.items.contains(oldItem))
    {
      return new Result(false, "Error: item is not in container");
    }

    return new Result(true);
  }

  private Result _updateItem(Item oldItem, Item newItem)
  {
    Result validUpdate = _validateUpdateItem(oldItem, newItem);
    if (!validUpdate.getStatus())
    {
      return validUpdate;
    }

    items.remove(oldItem);
    itemsByBarcode.remove(oldItem.getBarcode().getCode());
    itemsByProduct.remove(oldItem.getProduct());

    index(newItem);

    ItemsDAO dao = PersistenceMode.getSystemMode().getItemsDAO();
    dao.update(newItem);

    return new Result(true);
  }

  private Iterator<Item> _getItems()
  {
    return Collections.unmodifiableSet(items).iterator();
  }

  private String _generateNewItemBarcode()
  {
    return String.format("04%010d", currentItemBarcodeID++);
  }

  private Collection<ItemData> _getItems(ProductContainer container, Product product)
  {
    try
    {
      Iterator<Item> items;
      if (container == null){
        items = itemsByProduct.get(product).iterator();
      }
      else{
        items = container.getItems(product);
      }

      List<ItemData> itemData = new ArrayList<ItemData>();

      while (items.hasNext())
        itemData.add(items.next().generateItemDataObject());

      Collections.sort(itemData, getItemSorterByDate());

      return Collections.unmodifiableCollection(itemData);
    }
    catch (NullPointerException e)
    {
      return Collections.emptyList();
    }
  }

  private Result _validateEditItem(Item before, ItemData after)
  {
    if (after.getEntryDate() == null)
      return new Result(false, "Cannot edit item - entry date is null");

    return new Result(true);
  }

  private Result _editItem(Item before, ItemData data)
  {
    Result result = _validateEditItem(before, data);
    if (result.getStatus() == false)
      return result;

    Item after = before;
    before = new Item(after);
    after.setEntryDate(data.getEntryDate());

    EntityModifiedEvent event = generateItemModifiedEvent(before, after);
    fireModifiedEvent(event);

    return result;
  }

  private Item _getItem(String barcode)
  {
    return itemsByBarcode.get(barcode);
  }

  static void updateUnit(StorageUnit before, StorageUnit after)
  {
    INSTANCE._updateUnit(before, after);
  }

  private void _updateUnit(StorageUnit before, StorageUnit after)
  {
    Iterator<Item> items = before.getItems();
    while (items.hasNext())
    {
      Item item = items.next();
      item.setContainer(after);
    }
  }

  public static void save()
  {
    ItemsDAO dao = PersistenceMode.getSystemMode().getItemsDAO();

    Result result = dao.save(INSTANCE.items);
    if (!result.getStatus())
      System.err.println("Saving items failed: " + result.getMessage());

    result = dao.saveCurrentItemBarcodeID(INSTANCE.currentItemBarcodeID);
    if (!result.getStatus())
      System.err.println("Saving items failed: " + result.getMessage());
  }

  private void load()
  {
    ItemsDAO dao = PersistenceMode.getSystemMode().getItemsDAO();

    ValueResult<Set<Item>> itemsResult = dao.getAll();
    if (!itemsResult.getStatus())
    {
      System.err.println("Loading items failed: " + itemsResult.getMessage());
      return;
    }

    items = itemsResult.getValue();
    if(PersistenceMode.getSystemMode().name() == "DB"){
        for (Item item : items)
        {
        	item.setNewContainer(StorageUnitManager.getInstance().getContainer(item.getContainerId()));
        	StorageUnitManager.getInstance().getContainer(item.getContainerId()).addItem(item);
        }
    }
    removedItems.clear();
    itemsByBarcode.clear();
    itemsByProduct.clear();
    for (Item item : items)
    {
      if (item.isRemoved()) {
        removedItems.add(item);
        items.remove(item);
      }
      else {
        itemsByBarcode.put(item.getBarcode().getCode(), item);

        if (!itemsByProduct.containsKey(item.getProduct()))
          itemsByProduct.put(item.getProduct(), new HashSet<Item>());
        itemsByProduct.get(item.getProduct()).add(item);
      }
    }

    ValueResult<Integer> barcodeResult = dao.getCurrentItemBarcodeID();
    if (!barcodeResult.getStatus())
    {
      return;
    }

    currentItemBarcodeID = barcodeResult.getValue();
  }

  private INode acceptor = null;

  /**
   * Overrides the default implementation of the INode interface with the supplied INode
   * implementation. If the supplied INode is <tt>null</tt>, the default implementation is used.
   * 
   * @param acceptor
   *          The new implementation of the INode interface for this manager to use.
   */
  public void setVisitorAcceptor(INode acceptor)
  {
    this.acceptor = acceptor;
  }

  @Override
  public void preOrderAccept(IVisitor visitor)
  {
    if (acceptor == null)
    {
      for (Item item : items)
        item.preOrderAccept(visitor);

      for (Item item : removedItems)
        item.preOrderAccept(visitor);
    }
    else
      acceptor.preOrderAccept(visitor);
  }

  @Override
  public void postOrderAccept(IVisitor visitor)
  {
    if (acceptor == null)
    {
      for (Item item : items)
        item.preOrderAccept(visitor);

      for (Item item : removedItems)
        item.preOrderAccept(visitor);
    }
    else
      acceptor.postOrderAccept(visitor);
  }

}
